﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Reflection;

namespace JerrodSoft.Serialization.Xml
{
    public class XsdSerializer
    {
        public static readonly Dictionary<Type, string> XsdTypeMapping = new Dictionary<Type, string>()
        {
            {typeof(string), "string"},
            {typeof(decimal), "decimal"},
            {typeof(Int32), "integer"},
            {typeof(bool), "bool"},
            {typeof(DateTime), "dateTime"},
            {typeof(Int64), "long"},
            {typeof(Int16), "short"},
            {typeof(UInt16), "unsignedShort"},
            {typeof(UInt32), "unsignedInt"},
            {typeof(UInt64), "unsignedLong"},
            {typeof(byte), "byte"},
            {typeof(TimeSpan), "duration"}
        };

        public readonly string CustomXmlNamespace = "jsxns";
        public readonly string GuidRestrictionPattern = "\\{[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}\\}";
        public string Serialize(Type obj)
        {
            if (obj == null)
                return "";
            if (ObjectCache.IsPrimiviteType(obj))
                return "";
            if (ObjectCache.IsCollectionType(obj))
                throw new Exception("Need to support this later but for now it is not supported.");

            StringBuilder sb = new StringBuilder("<?xml version=\"1.0\">");
            sb.Append("<xs:schema elementFormDefault=\"qualified\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema" + "\">");
            sb.Append(this.Serialize(obj, new Hashtable(), sb));
            sb.Append("</xs:schema>");
            return sb.ToString().Replace("\"", @"""");
        }
        public string Serialize(params Type[] types)
        {
            if (types == null || types.Length == 0)
                return "";

            StringBuilder sb = new StringBuilder("<?xml version=\"1.0\">");
            sb.Append("<xs:schema elementFormDefault=\"qualified\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema" + "\">");
            var ht = new Hashtable();

            foreach (var obj in types)
            {
                if (obj == null)
                    return "";
                if (ObjectCache.IsPrimiviteType(obj))
                    return "";
                if (ObjectCache.IsCollectionType(obj))
                    throw new Exception("Need to support this later but for now it is not supported.");

                sb.Append(this.Serialize(obj, ht, sb));
            }
            sb.Append("</xs:schema>");
            return sb.ToString().Replace("\"", @"""");
        }

        private string Serialize(Type obj, Hashtable registeredTypes, StringBuilder tracker)
        {
            var type = ObjectCache.CacheType(obj);

            var nameAttribute = ObjectCache.GetAttribute<Attributes.XmlNameAttribute>(type.Target);
            string targetNamespace = "http://tempuri.org";
            string targetName = obj.Name;
            if (obj.FullName.Contains("<>f__AnonymousType"))
                targetName = "Anonymous" + Guid.NewGuid().ToString("N");

            if (nameAttribute != null)
            {
                if (!string.IsNullOrEmpty(nameAttribute.TargetNamespace))
                    targetNamespace = nameAttribute.TargetNamespace.TrimEnd(new char[] { '/', '\\' });
                if (!string.IsNullOrEmpty(nameAttribute.Name))
                {
                    targetName = nameAttribute.Name;
                    targetNamespace += "/" + nameAttribute.Name + ".xsd";
                }
            }
            else
                targetNamespace += "/" + type.Target.Name + ".xsd";

            if (!registeredTypes.ContainsKey(type.Target))
                registeredTypes.Add(type.Target, targetName);

            StringBuilder sb = new StringBuilder();

            sb.Append("<xs:element name=\"").Append(targetName).Append("\" nillable=\"true\" type=\"").Append(targetName).Append("\" />");  // assumption this is a complex type
            sb.Append("<xs:complexType name=\"").Append(targetName).Append("\"><xs:sequence>");

            foreach (var property in type.Target.GetProperties())
            {
                var propertyNameAttribute = ObjectCache.GetAttribute<Attributes.XmlNameAttribute>(property.PropertyType);
                string propertyName = property.Name;
                if (propertyNameAttribute != null && !string.IsNullOrEmpty(propertyNameAttribute.Name))
                    propertyName = propertyNameAttribute.Name;

                if (property.PropertyType.IsEnum)
                {
                    // simple type
                    if (!registeredTypes.ContainsKey(property.PropertyType))
                    {
                        tracker.Insert(tracker.Length, GetEnumXsdXmlComplexType(property.PropertyType, propertyName, propertyName));
                        registeredTypes.Add(property.PropertyType, propertyName);
                    }

                    sb.Append("<xs:element minOccurs=\"0\" maxOccurs=\"1\" name=\"").Append(propertyName).Append("\" type=\"").Append(GetTypeName(property.PropertyType)).Append("\" />");
                }
                else if (property.PropertyType.FullName == typeof(Guid).FullName)
                {
                    if (!registeredTypes.ContainsKey(typeof(Guid)))
                    {
                        tracker.Insert(tracker.Length, GetGuidComplexType(GuidRestrictionPattern));
                        registeredTypes.Add(typeof(Guid), "guid");
                    }
                    sb.Append("<xs:element minOccurs=\"0\" maxOccurs=\"1\" name=\"").Append(propertyName).Append("\" type=\"guid\" />");
                }
                else if (property.PropertyType.FullName == typeof(char).FullName)
                {
                    if (!registeredTypes.ContainsKey(typeof(char)))
                    {
                        tracker.Insert(tracker.Length, GetCharComplexType());
                        registeredTypes.Add(typeof(char), "char");
                    }
                    sb.Append("<xs:element minOccurs=\"0\" maxOccurs=\"1\" name=\"").Append(propertyName).Append("\" type=\"char\" />");
                }
                else if (ObjectCache.IsPrimiviteType(property.PropertyType))
                {
                    sb.Append("<xs:element minOccurs=\"0\" maxOccurs=\"1\" name=\"").Append(propertyName).Append("\" type=\"xs:").Append(GetTypeName(property.PropertyType)).Append("\"/>");
                }
                else if (ObjectCache.IsCollectionType(property.PropertyType))
                {
                    var collectionType = property.PropertyType.GetGenericArguments()[0];
                    var collectionPropertyName = "ArrayOf" + collectionType.Name;

                    if (collectionType.IsEnum)
                    {
                        StringBuilder propertyEnumSB = new StringBuilder();
                        if (!registeredTypes.ContainsValue(collectionType.Name))
                            tracker.Insert(tracker.Length, GetEnumXsdXmlComplexType(collectionType, collectionType.Name, collectionType.Name));

                        if (!registeredTypes.ContainsKey(property.PropertyType))
                        {
                            tracker.Insert(tracker.Length, GetBasicComplexType(collectionPropertyName, collectionPropertyName, true));

                            registeredTypes.Add(property.PropertyType, collectionPropertyName);
                        }

                        sb.Append("<xs:element minOccurs=\"0\" maxOccurs=\"1\" name=\"").Append(propertyName).Append("\" type=\"").Append(collectionPropertyName).Append("\" />");
                    }
                    else if (collectionType.FullName == typeof(Guid).FullName)
                    {
                        StringBuilder guidPropertySB = new StringBuilder();
                        if (!registeredTypes.ContainsKey(typeof(Guid)))
                        {
                            tracker.Insert(tracker.Length, GetGuidComplexType(GuidRestrictionPattern));
                            registeredTypes.Add(typeof(Guid), "guid");
                        }
                        if (!registeredTypes.ContainsKey(property.PropertyType))
                        {
                            tracker.Insert(tracker.Length, GetBasicComplexType(collectionPropertyName, "guid", true));

                            registeredTypes.Add(property.PropertyType, collectionPropertyName);
                        }

                        sb.Append("<xs:element minOccurs=\"0\" maxOccurs=\"1\" name=\"").Append(propertyName).Append("\" type=\"").Append(collectionPropertyName).Append("\" />");
                    }
                    else if (collectionType.FullName == typeof(char).FullName)
                    {
                        StringBuilder charPropertySB = new StringBuilder();
                        if (!registeredTypes.ContainsKey(typeof(char)))
                        {
                            tracker.Insert(tracker.Length, GetCharComplexType());
                            registeredTypes.Add(typeof(char), "char");
                        }
                        if (!registeredTypes.ContainsKey(property.PropertyType))
                        {
                            tracker.Insert(tracker.Length, GetBasicComplexType(collectionPropertyName, "char", true));

                            registeredTypes.Add(property.PropertyType, collectionPropertyName);
                        }

                        sb.Append("<xs:element minOccurs=\"0\" maxOccurs=\"1\" name=\"").Append(propertyName).Append("\" type=\"").Append(collectionPropertyName).Append("\" />");
                    }
                    else if (ObjectCache.IsPrimiviteType(collectionType))
                    {
                        if (!registeredTypes.ContainsValue(collectionPropertyName))
                        {
                            tracker.Insert(tracker.Length, GetBasicComplexType(collectionType.Name, collectionType.Name, true));

                            registeredTypes.Add(property.PropertyType, collectionPropertyName);
                        }
                        sb.Append("<xs:element minOccurs=\"0\" maxOccurs=\"1\" name=\"").Append(propertyName).Append("\" type=\"").Append(collectionPropertyName).Append("\"/>");
                    }
                    else if (ObjectCache.IsCollectionType(collectionType))
                        throw new Exception("You may not have arrays of arrays.");
                    else
                    {

                    }
                }
                else
                {
                    tracker.Insert(tracker.Length, this.Serialize(property.PropertyType, registeredTypes, tracker));
                    sb.Append("<xs:element minOccurs=\"0\" maxOccurs=\"1\" name=\"").Append(property.Name).Append(" type=\"").Append(registeredTypes[property.PropertyType] as string).Append("\"/>");
                }
            }

            sb.Append("</xs:sequence></xs:complexType>");
            return sb.ToString();
        }

        public string GetMethodXsdElements(Type type, Hashtable registeredTypes)
        {
            throw new NotImplementedException();
            StringBuilder sb = new StringBuilder();
            if (registeredTypes == null) registeredTypes = new Hashtable();

            foreach (var method in type.GetMethods().Where(d => d.IsPublic))
            {
                var operationAttribute = method.GetCustomAttributes(typeof(Attributes.OperationAttribute), true);
                if (operationAttribute != null && operationAttribute.Length > 0)
                {
                    Attributes.OperationAttribute attr = operationAttribute.First() as Attributes.OperationAttribute;
                    string name = method.Name;
                    if (!string.IsNullOrEmpty(attr.Name))
                        name = attr.Name;

                    bool canBeNull = !type.IsValueType || (Nullable.GetUnderlyingType(type) != null);
                    bool isPrimitiveReturnType = ObjectCache.IsPrimiviteType(method.ReturnType);

                    if (isPrimitiveReturnType)
                    {
                        sb.Append("<xs:element name=\"").Append(name).Append("Response\">\n");
                        sb.Append("<xs:complexType>\n");
                        sb.Append("<xs:sequence>\n");
                        sb.Append("<xs:element minOccurs=\"0\" name=\"value\" type=\"").Append(GetTypeName(method.ReturnType)).Append("\"/>\n");
                        sb.Append("</xs:element>\n");
                        sb.Append("</xs:sequence>\n");
                        sb.Append("</xs:complexType>\n");
                        sb.Append("</xs:element>\n");
                    }
                    else
                    {
                       // registeredTypes.Add(method.ReturnType, name);
                        sb.Insert(0, this.Serialize(method.ReturnType, registeredTypes, sb));
                    }

                    var inputParameters = method.GetParameters();
                    if (inputParameters != null && inputParameters.Length > 0)
                    {
                        sb.Append("<xs:element name=\"").Append(name).Append("Response\">\n");
                        sb.Append("<xs:complexType>\n");
                        sb.Append("<xs:sequence>\n");

                        foreach (var param in inputParameters)
                        {
                            bool isPrimitive = ObjectCache.IsPrimiviteType(param.ParameterType);
                            if (isPrimitive)
                            {
                                sb.Append("<xs:element minOccurs=\"0\" name=\"value\" type=\"").Append(GetTypeName(param.ParameterType)).Append("\"/>\n");
                            }
                            else
                            {
                                if (!registeredTypes.ContainsKey(param.ParameterType))
                                {
                                    // registeredTypes.Add(method.ReturnType, name);
                                    sb.Insert(0, this.Serialize(param.ParameterType, registeredTypes, sb));
                                }
                                else
                                {

                                }
                            }
                        } 
                        sb.Append("</xs:sequence>\n");
                        sb.Append("</xs:complexType>\n");
                        sb.Append("</xs:element>\n");
                    }
                }
            }

            return sb.ToString();
        }

        private string GetEnumXsdXmlComplexType(Type enumType, string name, string type)
        {
            if (!enumType.IsEnum)
                throw new Exception("Cannot get XSD Xml from non enum type via this method.");

            StringBuilder propertyEnumSB = new StringBuilder();
            propertyEnumSB.Append("<xs:element name=\"").Append(name).Append("\" type=\"").Append(type).Append("\" />");
            propertyEnumSB.Append("<xs:simpleType name=\"").Append(name).Append("\" >");
            propertyEnumSB.Append("<xs:restriction base=\"xs:string\">");
            foreach (var val in Enum.GetValues(enumType))
            {
                propertyEnumSB.Append("<xs:enumeration value=\"").Append(val.ToString()).Append("\" />");
            }
            propertyEnumSB.Append("</xs:restriction></xs:simpleType>");

            return propertyEnumSB.ToString();
        }

        private string GetGuidComplexType(string guidPattern)
        {
            StringBuilder guidPropertySB = new StringBuilder();
            guidPropertySB.Append("<xs:simpleType name=\"").Append("guid").Append("\">");
            guidPropertySB.Append("<xs:restriction base=\"xs:string\">");
            guidPropertySB.Append("<xs:pattern value=\"").Append(guidPattern).Append("\" />");
            guidPropertySB.Append("</xs:restriction>");
            guidPropertySB.Append("</xs:simpleType>");

            return guidPropertySB.ToString();
        }

        private string GetCharComplexType()
        {
            StringBuilder charPropertySB = new StringBuilder();
            charPropertySB.Append("<xs:simpleType name=\"").Append("char").Append("\">");
            charPropertySB.Append("<xs:restriction base=\"xs:string\">");
            charPropertySB.Append("<xs:length value=\"").Append("1").Append("\" />");
            charPropertySB.Append("</xs:restriction>");
            charPropertySB.Append("</xs:simpleType>");

            return charPropertySB.ToString();
        }

        private string GetBasicComplexType(string name, string type, bool isArray)
        {
            StringBuilder sb = new StringBuilder();
            sb.Append("<xs:complexType name=\"").Append(name).Append("\">");
            sb.Append("<xs:sequence>");
            sb.Append("<xs:element minOccurs=\"0\"").Append(isArray ? " maxOccurs=\"unbounded\" " : " maxOccurs=\"1\" ").Append("name=\"").Append(name).Append("\" type=\"").Append(type).Append("\" />");
            sb.Append("</xs:sequence>");
            sb.Append("</xs:complexType>");

            return sb.ToString();
        }

        public static string GetTypeName(Type type)
        {
            if (XsdTypeMapping.ContainsKey(type))
                return XsdTypeMapping[type];
            else
                return type.Name;
        }
    }
}
